using System;
using System.Collections.Generic;
using System.Text;
using System.Data;
using System.Diagnostics;

namespace PortfolioAllocator.Optimization
{
    class OptimizationRunner
    {
        static private readonly TraceSwitch traceM = new TraceSwitch("OptimizationRunner", "Debug messages from OptimizationRunner");
        static public readonly string TICKER_COL = "Ticker";
        static public readonly string VOL_COL = "Vol";
        static public readonly string EXP_R_COL = "<r>";
        static public readonly string EXP_LN_R_COL = "<ln(r)>";
        static public readonly string MIN_VAR_W_COL = "w(MinVar)";
        static public readonly string KELLY_W_COL = "w(Kelly)";
        static public readonly string DATE_COL = "Date";
        static public readonly string DAILY_RET = "DailyRet";
        static public readonly string CUMUL_RET = "CumRet";

        public static readonly string FORMAT_EXT_PROPERTY = "Format";

        static private readonly int nExpCovM = 60;  // period for exponentially weighted covariance matrix calculation

        /// <summary>Encapsulates input data for Optimization</summary>
        private struct InputData
        {
            #region Private Members
            private int nOfInstrM;      // nuber of instrumets (= size of arrays)
            private double[] rM;        // expected returns
            private string[] tickersM;
            DateMultiValues pricesM;    // table with underlying prices
            DateMultiValues logRetsM;   // table with log-returns
            private Dictionary<string, int> tickerToIxM; 
            private List<PositionConstraint> constraintsM;
            #endregion
            public InputData(int nOfInstruments)
            {
                this.nOfInstrM = nOfInstruments;
                this.rM = new double[nOfInstruments];
                this.tickersM = new string[nOfInstruments];
                this.tickerToIxM = new Dictionary<string,int>();
                this.logRetsM = new DateMultiValues(new List<DateTime>(0), new double[0,nOfInstruments]);
                this.pricesM = new DateMultiValues(new List<DateTime>(0), new double[0, nOfInstruments]);
                this.constraintsM = new List<PositionConstraint>();
            }
            public int NOfInstr
            {
                get { return this.nOfInstrM; }
            }
            public int NOfDates
            {
                get { return this.logRetsM.Dates.Count; }
            }
            public double[] ExpRets
            {
                get { return this.rM; }
                set { this.rM = value; }
            }
            public string[] Tickers
            {
                get { return this.tickersM; }
                set { this.tickersM = value; }
            }
            public DateMultiValues LogRets
            {
                get { return this.logRetsM; }
                set { this.logRetsM = value; }
            }
            public DateMultiValues Prices
            {
                get { return this.pricesM; }
                set { this.pricesM = value; }
            }
            public Dictionary<string, int> TickerToIx
            {
                get { return this.tickerToIxM; }
                set { this.tickerToIxM = value; }
            }
            public List<PositionConstraint> Constraints
            {
                get { return this.constraintsM; }
                set { this.constraintsM = value; }
            }
        }

        /// <summary>Encapsulates PositionConstraint using string tickers instead of numerical indexes. Used form GUI</summary>
        public struct PConstraint
        {
            private double minM;
            private double maxM;
            private List<string> tickersM;
            public PConstraint(double min, double max, List<string> tickers)
            {
                this.minM = min;
                this.maxM = max;
                this.tickersM = tickers;
            }
            public PConstraint(double min, double max, string[] tickers)
                : this(min, max, new List<string>(tickers))
            { }
            public double Min
            {
                get { return this.minM; }
            }
            public double Max
            {
                get { return this.maxM; }
            }
            public List<string> Tickers
            {
                get { return this.tickersM; }
            }
        }

        /// <summary>Perform single step min variance optimization analysis. Calculate minVar and Kelly optimal portfolios. Generate Efficient frontier</summary>
        /// <param name="inTable">
        /// DataTable with the following columns:
        /// col 1: Loader (int corresponding to enum TALoaders.Loader)
        /// col 2: Ticker
        /// col 3: Expected (single period) return
        /// col 4: min weight constraint
        /// col 5: max weight constraint
        /// </param>
        /// <param name="startDate">The start date for data load and covariance matrix construction</param>
        /// <param name="endDate">Evaluation date</param>
        /// <param name="constrs">List of optimization constraints from GUI</param>
        public DataTable runMinVarianceAnalysis(DataTable inTable, DateTime startDate, DateTime endDate, List<PConstraint> constrs)
        {
            // 1. Load input data from inTable
            InputData input = this.loadInputData(inTable, startDate, endDate, constrs    );
            if (input.NOfInstr == 0)
                return new DataTable();

            // 2. Generate Covariance Matrix
            int startIx = 0;
            int endIx = input.LogRets.NOfRows;
            double[,] covStart = MultiValueUtilities.createWarmUpCovarianceMatrix(input.NOfInstr);

            double[,] covDaily = MultiValueUtilities.calcExpCovarAB(input.LogRets.Values, covStart, startIx, endIx, nExpCovM);
            double[,] cov = MultiValueUtilities.annualizeCovarianceMatrix(covDaily);

            // Solve Min Variance and Kelly Optimal portfolio weights
            AllocationSolver aSolver = new AllocationSolver();
            double[] wMinVar = aSolver.findMinVariancePortfWeights(cov, input.Constraints);
            // Solve Kelly Optimal portfolio
            double[] wKelly = aSolver.findMaxLogReturnPortfWeights(cov, input.ExpRets, input.Constraints);

            #region Create output DataTable
            // Create DataTable with ticker/portf,vol,<r>,<lnR>,MinVarW,KellyW
            DataTable outTable = new DataTable();
            outTable.Columns.Add(TICKER_COL, typeof(string));
            outTable.Columns.Add(VOL_COL, typeof(double)).ExtendedProperties.Add(FORMAT_EXT_PROPERTY, "P2");
            outTable.Columns.Add(EXP_R_COL, typeof(double)).ExtendedProperties.Add(FORMAT_EXT_PROPERTY, "P2");
            outTable.Columns.Add(EXP_LN_R_COL, typeof(double)).ExtendedProperties.Add(FORMAT_EXT_PROPERTY, "P2");
            outTable.Columns.Add(MIN_VAR_W_COL, typeof(double)).ExtendedProperties.Add(FORMAT_EXT_PROPERTY, "P1");
            outTable.Columns.Add(KELLY_W_COL, typeof(double)).ExtendedProperties.Add(FORMAT_EXT_PROPERTY, "P1");

            DataRow row;
            for (int i = 0; i < input.NOfInstr; ++i)
            {
                row = outTable.NewRow();
                row[TICKER_COL] = input.Tickers[i];
                row[VOL_COL] = Math.Sqrt(cov[i, i]);
                row[EXP_R_COL] = input.ExpRets[i];
                row[EXP_LN_R_COL] = input.ExpRets[i] - 0.5 * cov[i, i];
                row[MIN_VAR_W_COL] = wMinVar[i];
                row[KELLY_W_COL] = wKelly[i];
                outTable.Rows.Add(row);
            }
            // Min Variance row:
            row = outTable.NewRow();
            row[TICKER_COL] = "MinVariance";
            row[VOL_COL] = aSolver.calcVolatility(wMinVar, cov);
            row[EXP_R_COL] = aSolver.calcExpReturn(wMinVar, input.ExpRets);
            row[EXP_LN_R_COL] = aSolver.calcExpLogReturn(wMinVar, input.ExpRets, cov);
            row[MIN_VAR_W_COL] = 1.00;
            row[KELLY_W_COL] = 0.00;
            outTable.Rows.Add(row);
            // Kelly row
            row = outTable.NewRow();
            row[TICKER_COL] = "Kelly";
            row[VOL_COL] = aSolver.calcVolatility(wKelly, cov);
            row[EXP_R_COL] = aSolver.calcExpReturn(wKelly, input.ExpRets);
            row[EXP_LN_R_COL] = aSolver.calcExpLogReturn(wKelly, input.ExpRets, cov);
            row[MIN_VAR_W_COL] = 0.00;
            row[KELLY_W_COL] = 1.00;
            outTable.Rows.Add(row);
            #endregion
            // return results
            return outTable;
        }

        /// <summary>Back-test MinVariance strategy with monthly rebalancing</summary>
        /// <param name="inTable"></param>
        /// DataTable with the following columns:
        /// col 1: Loader (int corresponding to enum TALoaders.Loader)
        /// col 2: Ticker
        /// col 3: Expected (single period) return
        /// col 4: min weight constraint
        /// col 5: max weight constraint
        /// <param name="startDate">The start date back-testing</param></param>
        /// <param name="endDate">The end date for back-testing</param>
        public DataTable runMinVarBackTest(DataTable inTable, DateTime startDate, DateTime endDate, List<PConstraint> constrs)
        {
            Debug.WriteLineIf(traceM.TraceInfo, "Entering runMinVarBackTest(inTable, " + startDate.ToShortDateString() + ", " + endDate.ToShortDateString() + ", constr.Count=" + constrs.Count + ")");
            Debug.Indent();
            // 1. Load input data from inTable
            DateTime loadStartDate = startDate.AddYears(-1); // warm up data
            InputData input = this.loadInputData(inTable, loadStartDate, endDate, constrs);
            if (input.NOfInstr == 0)
                return new DataTable();

            // 2. Find indexes for rebalancing dates
            List<int> indexes = this.findRebalancingIndexes(input.LogRets.Dates, startDate, Frequency.Monthly);
            if (indexes.Count == 0)
                return new DataTable();

            // 3. Iterate over all rebalancing dates, calculate MinVar weights and cumulative returns
            int lastIx = 0;     // index of the previous rebalancing date
            double[,] covDaily = MultiValueUtilities.createWarmUpCovarianceMatrix(input.NOfInstr); // create start-up covariance matrix 
            double[] dailyRet = new double[input.NOfDates]; // Strategy's daily returns on each date index
            double[] cumRet = new double[input.NOfDates];   // Strategy's cumulative return since startIx
            double[,] weights = new double[input.NOfDates, input.NOfInstr]; // Portfolio weights
            AllocationSolver aSolver = new AllocationSolver();
            #region Debug print
            if (traceM.TraceVerbose)
            {
                Debug.WriteLine("Generating MinVar weights on rebalancing dates");
                Debug.Write("ix,date");
                for (int instrIx = 0; instrIx < input.NOfInstr; ++instrIx)
                    Debug.Write( ",w(" + input.Tickers[instrIx] + ")");
                Debug.WriteLine("");
            }
            #endregion
            for (int i = 0; i < indexes.Count - 1; ++i)
            {
                int ix = indexes[i];        // this rebalancing index
                // 3.1 Generate Covariance matrix at rebalancing date
                Debug.WriteIf(traceM.TraceVerbose, ix + "," + input.Prices.Dates[ix].ToShortDateString());
                covDaily = MultiValueUtilities.calcExpCovarAB(input.LogRets.Values, covDaily, lastIx, ix, nExpCovM);
                lastIx = ix;        // update lastIx
                double[,] cov = MultiValueUtilities.annualizeCovarianceMatrix(covDaily);

                // 3.2 Find Min Var portfolio at rebalancing date
                double[] wMinVar = aSolver.findMinVariancePortfWeights(cov, input.Constraints);
                #region Debug print
                if (traceM.TraceVerbose)
                {
                    for (int instrIx = 0; instrIx < input.NOfInstr; ++instrIx)
                        Debug.Write("," + wMinVar[instrIx].ToString("0.0000"));
                    Debug.WriteLine("");
                }
                #endregion

                // 3.3 Calculate daily and cumulative returns until the next rebalancing date
                int nextIx = indexes[i + 1];
                for (int dateIx = ix + 1; dateIx < nextIx + 1; ++dateIx)
                {
                    double normalizationFactor = 0;
                    for (int instrIx = 0; instrIx < input.NOfInstr; ++instrIx)
                    {
                        double w = wMinVar[instrIx] * (input.Prices.Values[dateIx - 1, instrIx] / input.Prices.Values[ix, instrIx]);
                        weights[dateIx - 1, instrIx] = w;
                        normalizationFactor += w;
                        cumRet[dateIx] += wMinVar[instrIx] * (input.Prices.Values[dateIx, instrIx] / input.Prices.Values[ix, instrIx] - 1);
                    }
                    // Normalize weights
                    for (int instrIx = 0; instrIx < input.NOfInstr; ++instrIx)
                        weights[dateIx - 1, instrIx] /= normalizationFactor;
                    
                    cumRet[dateIx] = (1+cumRet[ix])*(1+cumRet[dateIx]) - 1;   // multiply by return until dates[ix]
                    dailyRet[dateIx] = (1 + cumRet[dateIx]) / (1 + cumRet[dateIx - 1]) - 1;
                } 
             }

            // 4. Create DataTable with back-test results: date, daily return, cumulReturn, (asset weights)
            DataTable result = new DataTable();
            result.Columns.Add(DATE_COL, typeof(DateTime));
            result.Columns.Add(DAILY_RET, typeof(double)).ExtendedProperties.Add(FORMAT_EXT_PROPERTY, "P2");
            result.Columns.Add(CUMUL_RET, typeof(double)).ExtendedProperties.Add(FORMAT_EXT_PROPERTY, "P2");
            foreach (string ticker in input.Tickers)
                result.Columns.Add(ticker, typeof(double)).ExtendedProperties.Add(FORMAT_EXT_PROPERTY, "P2");

            #region Debug print
            if (traceM.TraceVerbose)
            {
                Debug.Write("ix,date,rDaily,rCum");
                foreach (string ticker in input.Tickers)
                    Debug.Write(",w(" + ticker + ")");
                foreach (string ticker in input.Tickers)
                    Debug.Write(",P(" + ticker + ")");
                Debug.WriteLine("");
            }
            #endregion
            for (int dateIx = indexes[0]; dateIx < input.NOfDates; ++dateIx)
            {
                DataRow row = result.NewRow();
                row[DATE_COL] = input.Prices.Dates[dateIx];
                row[DAILY_RET] = Math.Round(dailyRet[dateIx],6);
                row[CUMUL_RET] = Math.Round(cumRet[dateIx],6);
                for (int instrIx = 0; instrIx < input.NOfInstr; ++instrIx)
                    row[input.Tickers[instrIx]] = Math.Round(weights[dateIx, instrIx], 6);
                result.Rows.Add(row);
                #region Debug print
                if (traceM.TraceVerbose)
                {
                    Debug.Write(dateIx + "," + input.Prices.Dates[dateIx].ToShortDateString() + "," + dailyRet[dateIx].ToString("0.0000") + "," + cumRet[dateIx].ToString("0.0000"));
                    for (int instrIx = 0; instrIx < input.NOfInstr; ++instrIx)
                        Debug.Write("," + weights[dateIx, instrIx].ToString("0.0000"));
                    for (int instrIx = 0; instrIx < input.NOfInstr; ++instrIx)
                        Debug.Write("," + input.Prices.Values[dateIx, instrIx].ToString("0.00"));
                    Debug.WriteLine("");
                }
                #endregion
            }

            Debug.Unindent();
            Debug.WriteLineIf(traceM.TraceInfo, "returning DataTable(" + result.Rows.Count + ", " + result.Columns.Count + ")");
            return result;
        }

        /// <summary>Process input DataTable and create InputData struct
        /// </summary>
        /// <param name="inTable">DataTable with the following columns:
        /// col 1: Loader (int corresponding to enum TALoaders.Loader)
        /// col 2: Ticker
        /// col 3: Expected (single period) return
        /// col 4: min weight constraint
        /// col 5: max weight constraint</param>
        /// <param name="startDate">The start date for data load and covariance matrix construction</param>
        /// <param name="endDate">Evaluation date</param>
        /// <param name="constrs">List of optimization constraints from GUI</param>
        /// <returns></returns>
        private InputData loadInputData(DataTable inTable, DateTime startDate, DateTime endDate, List<PConstraint> constrs)
        {
            Debug.WriteLineIf(traceM.TraceInfo, "Entering loadInputData(inData[" + inTable.Rows.Count + "," + inTable.Columns.Count + "], " + startDate.ToShortDateString() + ", " + endDate.ToShortDateString());
            // Convert inData rows into input variable
            int nOfInstr = inTable.Rows.Count;
            if (inTable.Columns.Count != 5)
                throw new Exception("Wrong number of columns in inData DataTable. inData.Columns.Count=" + inTable.Columns.Count + ", expected 5");

            InputData result = new InputData(nOfInstr);
            List<SortedList<DateTime, double>> timeSeriesList = new List<SortedList<DateTime, double>>(nOfInstr);
            Debug.WriteLineIf(traceM.TraceVerbose, "rowIx,loader,ticker,rExp,minX,maxX");
            for (int rowIx = 0; rowIx < nOfInstr; ++rowIx)
            {
                DataRow row = inTable.Rows[rowIx];
                TALoaders.Loader loaderType = (TALoaders.Loader)Enum.Parse(typeof(TALoaders.Loader), Convert.ToString(row[0]));
                TALoaders.BaseLoader loader = TALoaders.DownloaderFactory.createCachedDownloader(loaderType.ToString());
                string ticker = Convert.ToString(row[1]);
                result.Tickers[rowIx] = ticker;
                result.TickerToIx[ticker] = rowIx;
                result.ExpRets[rowIx] = Convert.ToDouble(row[2]);
                double xMin = Convert.ToDouble(row[3]);
                double xMax = Convert.ToDouble(row[4]);
                result.Constraints.Add(new PositionConstraint(xMin, xMax, new int[] { rowIx })); // Add position constraints
                Debug.WriteLineIf(traceM.TraceVerbose, rowIx + "," + loader.ToString() + "," + result.Tickers[rowIx] + "," + result.ExpRets[rowIx] + "," + xMin + "," + xMax);
                SortedList<DateTime, double> timeSeries = loader.getHistoricalPrices(result.Tickers[rowIx], startDate, endDate, TALoaders.QuoteType.AdjClose);
                timeSeriesList.Add(timeSeries);
            }

            // Add position constraints
            foreach (PConstraint c in constrs)
            {
                List<int> indexes = new List<int>(c.Tickers.Count);
                int ix;
                foreach (string ticker in c.Tickers)
                    if (result.TickerToIx.TryGetValue(ticker, out ix))
                        indexes.Add(ix);
                    else
                        Debug.WriteLineIf(traceM.TraceError, "Missing ticker " + ticker + ". IGNORING");
                
                result.Constraints.Add(new PositionConstraint(c.Min, c.Max, indexes));
            }

            // calculate Log-Returns
            result.Prices = MultiValueUtilities.findIntersection(timeSeriesList);
            result.LogRets = MultiValueUtilities.calcLogReturns(result.Prices);

            return result;
        }

        private enum Frequency { Monthly, Quarterly, Yearly };

        /// <summary>Find list of indexes for rebalancing dates. The last index corresponds to the endDate</summary>
        /// <param name="dates">List of all available dates</param>
        /// <param name="startDate">The first rebalancing date</param>
        /// <param name="freq">Rebalancing frequency</param>
        /// <returns>List of indexes corresponding to teh rebalancing dates. The last index corresponds to the end date</returns>
        private List<int> findRebalancingIndexes(List<DateTime> dates, DateTime startDate, Frequency freq)
        {
            Debug.WriteLineIf(traceM.TraceInfo, "Entering findRebalancingIndexes(dates.Count=" + dates.Count + ", " + startDate.ToShortDateString() + ", " + freq.ToString() + ")");
            List<int> indexes = new List<int>();
            DateTime nextDate = startDate;
            // Error handling
            if (dates.Count == 0)
                throw new Exception("No historical data available");
            if (dates[0] > nextDate)
                throw new Exception("The start date " + nextDate.ToShortDateString() + " is before the first historical data point" + dates[0].ToShortDateString());
            
            for (int ix = 0; ix < dates.Count-1; ++ix)
            {
                if (dates[ix] <= nextDate && dates[ix + 1] > nextDate)
                {
                    indexes.Add(ix);
                    Debug.WriteIf(traceM.TraceVerbose, "Adding index ix=" + ix + ", date=" + dates[ix].ToShortDateString() + ". ");
                    switch (freq)
                    {
                        case Frequency.Monthly:
                            nextDate = nextDate.AddMonths(1);
                            break;
                        case Frequency.Quarterly:
                            nextDate = nextDate.AddMonths(3);
                            break;
                        case Frequency.Yearly:
                            nextDate = nextDate.AddYears(1);
                            break;
                        default:
                            throw new Exception("Unrecognized rebalancing frequency " + freq.ToString());
                    }
                    Debug.WriteLineIf(traceM.TraceVerbose, "NextDate=" + nextDate.ToShortDateString());
                }
            }

            indexes.Add(dates.Count-1); // add the last index
            Debug.WriteLineIf(traceM.TraceInfo, "...found " + indexes.Count + " indexes");
            return indexes;
        }
    }
}
